Micron Document
🎖️GitЯра🎖️

Commit b307ceb5c24b98fa5c8a49fdf95e56eeaa4187bb


Parents : cc4debd
Author : James Rich <2199651+jamesarich@users.noreply.github.com>
Signature : Signature validation error
Date : 2026-07-10T21:40:33-05:00
Committer : GitHub <noreply@github.com>
Date : 2026-07-11T02:40:33Z

feat(desktop): update notifications and store-readiness fixes (#6215)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

Changes
Diff

diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml
index e3d8a8ac7f..c278a4bded 100644
--- a/.github/workflows/pull-request.yml
+++ b/.github/workflows/pull-request.yml
@@ -114,6 +114,17 @@ jobs:
- uses: actions/checkout@v7
- name: Validate store listing metadata lengths
run: python3 scripts/check-metadata-length.py
+ # Flathub/AppStream needs a <release> entry for the version being shipped; a stale
+ # <releases> block degrades (or fails) the Flathub listing. Fails the PR that bumps
+ # VERSION_NAME_BASE until the matching entry is added.
+ - name: Require AppStream release entry for current version
+ run: |
+ VERSION=$(grep '^VERSION_NAME_BASE=' config.properties | cut -d'=' -f2)
+ METAINFO=desktopApp/packaging/linux/org.meshtastic.MeshtasticDesktop.metainfo.xml
+ if ! grep -q "version=\"$VERSION\"" "$METAINFO"; then
+ echo "::error file=$METAINFO::Missing <release version=\"$VERSION\"> entry. Add it alongside the VERSION_NAME_BASE bump."
+ exit 1
+ fi
# 2. VALIDATION & BUILD: Delegate to reusable-check.yml
# We disable coverage and desktop builds for PRs to keep feedback fast

diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 743eb4e5f5..4ae68689a8 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -306,7 +306,7 @@ jobs:
run: >
./gradlew :desktopApp:packageReleaseDistributionForCurrentOS
${{ contains(runner.os, 'macOS') && ':desktopApp:packageReleaseUberJarForCurrentOS' || '' }}
- '-PaboutLibraries.release=true' --no-daemon
+ '-PaboutLibraries.release=true' '-Pdesktop.release=true' --no-daemon
# jpackage computes the .deb Depends: line from the build host's package names.
# Ubuntu 24.04 records time_t64 transition names (libasound2t64, libpng16-16t64)

diff --git a/.skills/compose-ui/strings-index.txt b/.skills/compose-ui/strings-index.txt
index 11e5782495..f0821be06d 100644
--- a/.skills/compose-ui/strings-index.txt
+++ b/.skills/compose-ui/strings-index.txt
@@ -334,10 +334,14 @@ demo_mode
demo_mode_replay
desc_node_filter_clear
description
+### DESKTOP ###
desktop_notification_title
desktop_tray_quit
desktop_tray_show
desktop_tray_tooltip
+desktop_update_available_message
+desktop_update_available_title
+desktop_update_download
details
detection_sensor
detection_sensor_config

diff --git a/core/resources/src/commonMain/composeResources/values/strings.xml b/core/resources/src/commonMain/composeResources/values/strings.xml
index e832eece77..2550de7df3 100644
--- a/core/resources/src/commonMain/composeResources/values/strings.xml
+++ b/core/resources/src/commonMain/composeResources/values/strings.xml
@@ -358,10 +358,14 @@
<string name="demo_mode_replay">Demo Mode (Replay)</string>
<string name="desc_node_filter_clear">clear node filter</string>
<string name="description">Description</string>
+ <!-- DESKTOP -->
<string name="desktop_notification_title">Meshtastic</string>
<string name="desktop_tray_quit">Quit</string>
<string name="desktop_tray_show">Show Meshtastic</string>
<string name="desktop_tray_tooltip">Meshtastic Desktop</string>
+ <string name="desktop_update_available_message">Meshtastic Desktop %1$s is ready to download.</string>
+ <string name="desktop_update_available_title">App update available</string>
+ <string name="desktop_update_download">Download update %1$s</string>
<string name="details">Details</string>
<string name="detection_sensor">Detection Sensor</string>
<string name="detection_sensor_config">Detection Sensor Config</string>

diff --git a/desktopApp/packaging/linux/org.meshtastic.MeshtasticDesktop.metainfo.xml b/desktopApp/packaging/linux/org.meshtastic.MeshtasticDesktop.metainfo.xml
index e3dfda7f56..9277b1af26 100644
--- a/desktopApp/packaging/linux/org.meshtastic.MeshtasticDesktop.metainfo.xml
+++ b/desktopApp/packaging/linux/org.meshtastic.MeshtasticDesktop.metainfo.xml
@@ -92,6 +92,11 @@
</screenshots>
<releases>
+ <release version="2.8.0" date="2026-07-10">
+ <description>
+ <p>In-app update notifications, native OS notifications, deep-link handling, keyboard shortcuts, window-state persistence, and many stability fixes.</p>
+ </description>
+ </release>
<release version="2.7.14" date="2026-06-03">
<description>
<p>Initial release of Meshtastic Desktop.</p>

diff --git a/desktopApp/src/main/kotlin/org/meshtastic/desktop/Main.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/Main.kt
index f71126606a..86b828c96d 100644
--- a/desktopApp/src/main/kotlin/org/meshtastic/desktop/Main.kt
+++ b/desktopApp/src/main/kotlin/org/meshtastic/desktop/Main.kt
@@ -24,6 +24,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
@@ -66,6 +67,7 @@ import io.ktor.client.HttpClient
import kotlinx.coroutines.flow.first
import okio.Path.Companion.toPath
import org.jetbrains.compose.resources.decodeToSvgPainter
+import org.jetbrains.compose.resources.getString
import org.jetbrains.compose.resources.stringResource
import org.koin.compose.koinInject
import org.koin.core.context.startKoin
@@ -77,14 +79,19 @@ import org.meshtastic.core.navigation.MultiBackstack
import org.meshtastic.core.navigation.SettingsRoute
import org.meshtastic.core.navigation.TopLevelDestination
import org.meshtastic.core.navigation.rememberMultiBackstack
+import org.meshtastic.core.repository.Notification
import org.meshtastic.core.repository.UiPrefs
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.desktop_tray_quit
import org.meshtastic.core.resources.desktop_tray_show
import org.meshtastic.core.resources.desktop_tray_tooltip
+import org.meshtastic.core.resources.desktop_update_available_message
+import org.meshtastic.core.resources.desktop_update_available_title
+import org.meshtastic.core.resources.desktop_update_download
import org.meshtastic.core.service.MeshServiceOrchestrator
import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.core.ui.util.LocalEventBranding
+import org.meshtastic.core.ui.util.rememberOpenUrl
import org.meshtastic.core.ui.viewmodel.UIViewModel
import org.meshtastic.desktop.data.DesktopPreferencesDataSource
import org.meshtastic.desktop.di.desktopModule
@@ -223,6 +230,9 @@ private fun ApplicationScope.MeshtasticDesktopApp(uiViewModel: UIViewModel, isDa
notificationManager.fallbackNotifications.collect { notification -> trayState.sendNotification(notification) }
}
+ val openUrl = rememberOpenUrl()
+ val updateInfo = rememberAvailableUpdate(notificationManager)
+
WindowBoundsManager(desktopPrefs, windowState) { isWindowReady = true }
Tray(
@@ -231,6 +241,12 @@ private fun ApplicationScope.MeshtasticDesktopApp(uiViewModel: UIViewModel, isDa
tooltip = stringResource(Res.string.desktop_tray_tooltip),
onAction = { isAppVisible = true },
menu = {
+ updateInfo?.let { update ->
+ Item(
+ stringResource(Res.string.desktop_update_download, update.versionName),
+ onClick = { openUrl(update.releaseUrl) },
+ )
+ }
Item(stringResource(Res.string.desktop_tray_show), onClick = { isAppVisible = true })
Item(stringResource(Res.string.desktop_tray_quit), onClick = ::exitApplication)
},
@@ -252,6 +268,39 @@ private fun ApplicationScope.MeshtasticDesktopApp(uiViewModel: UIViewModel, isDa
}
}
+// ----- Update discovery -----
+
+/**
+ * Checks once per launch whether a newer release is published on GitHub and surfaces it via a native notification.
+ * Release builds only — dev builds carry the bare base version and would match or trail every published release.
+ * Flatpak installs (FLATPAK_ID is set inside the sandbox) update through Flathub instead, so don't point them at
+ * GitHub.
+ */
+@Composable
+private fun rememberAvailableUpdate(notificationManager: DesktopNotificationManager): UpdateChecker.UpdateInfo? {
+ val buildConfig = koinInject<BuildConfigProvider>()
+ val httpClient = koinInject<HttpClient>()
+ val updateInfo by
+ produceState<UpdateChecker.UpdateInfo?>(initialValue = null) {
+ if (!buildConfig.isDebug && System.getenv("FLATPAK_ID") == null) {
+ value = UpdateChecker(httpClient).check(buildConfig.versionName)
+ }
+ }
+
+ LaunchedEffect(updateInfo) {
+ updateInfo?.let { update ->
+ notificationManager.dispatch(
+ Notification(
+ title = getString(Res.string.desktop_update_available_title),
+ message = getString(Res.string.desktop_update_available_message, update.versionName),
+ category = Notification.Category.Service,
+ ),
+ )
+ }
+ }
+ return updateInfo
+}
+
// ----- Window bounds persistence -----
/** Restores window geometry from preferences and persists changes via [snapshotFlow]. */

diff --git a/desktopApp/src/main/kotlin/org/meshtastic/desktop/UpdateChecker.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/UpdateChecker.kt
new file mode 100644
index 0000000000..08c637b55e
--- /dev/null
+++ b/desktopApp/src/main/kotlin/org/meshtastic/desktop/UpdateChecker.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (c) 2026 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package org.meshtastic.desktop
+
+import co.touchlab.kermit.Logger
+import io.ktor.client.HttpClient
+import io.ktor.client.request.get
+import io.ktor.client.statement.bodyAsText
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.json.jsonObject
+import kotlinx.serialization.json.jsonPrimitive
+
+/**
+ * Checks GitHub Releases for a newer published desktop build.
+ *
+ * The `releases/latest` endpoint only ever returns a published, non-draft, non-prerelease release, so users are only
+ * pointed at builds that completed the full promotion pipeline (see `.github/workflows/promote.yml`).
+ */
+class UpdateChecker(private val httpClient: HttpClient) {
+
+ /** A published release newer than the running build. */
+ data class UpdateInfo(val versionName: String, val releaseUrl: String)
+
+ /** Returns the latest published release when it is newer than [currentVersionName], null otherwise. */
+ suspend fun check(currentVersionName: String): UpdateInfo? = runCatching {
+ val release = Json.parseToJsonElement(httpClient.get(LATEST_RELEASE_URL).bodyAsText()).jsonObject
+ val tag = release["tag_name"]?.jsonPrimitive?.content
+ if (tag != null && isNewer(latest = tag, current = currentVersionName)) {
+ UpdateInfo(
+ versionName = tag.removePrefix("v"),
+ releaseUrl = release["html_url"]?.jsonPrimitive?.content ?: RELEASES_PAGE_URL,
+ )
+ } else {
+ null
+ }
+ }
+ .getOrElse { e ->
+ Logger.d(e) { "Update check skipped: offline, rate-limited, or malformed response" }
+ null
+ }
+
+ companion object {
+ private const val LATEST_RELEASE_URL =
+ "https://api.github.com/repos/meshtastic/Meshtastic-Android/releases/latest"
+ private const val RELEASES_PAGE_URL = "https://github.com/meshtastic/Meshtastic-Android/releases"
+
+ private val VERSION_REGEX = Regex("""(\d+)\.(\d+)\.(\d+)""")
+
+ /**
+ * True when [latest] carries a strictly newer numeric X.Y.Z than [current]. Suffixes (`v`, `-internal.N`) are
+ * ignored; versions that don't parse never trigger an update.
+ */
+ internal fun isNewer(latest: String, current: String): Boolean {
+ val latestVersion = parse(latest)
+ val currentVersion = parse(current)
+ return latestVersion != null &&
+ currentVersion != null &&
+ compareValuesBy(latestVersion, currentVersion, { it.first }, { it.second }, { it.third }) > 0
+ }
+
+ private fun parse(raw: String): Triple<Int, Int, Int>? =
+ VERSION_REGEX.find(raw)?.destructured?.let { (major, minor, patch) ->
+ // runCatching: \d+ can match components too large for Int; treat those as unparseable.
+ runCatching { Triple(major.toInt(), minor.toInt(), patch.toInt()) }.getOrNull()
+ }
+ }
+}

diff --git a/desktopApp/src/test/kotlin/org/meshtastic/desktop/UpdateCheckerTest.kt b/desktopApp/src/test/kotlin/org/meshtastic/desktop/UpdateCheckerTest.kt
new file mode 100644
index 0000000000..c698f40344
--- /dev/null
+++ b/desktopApp/src/test/kotlin/org/meshtastic/desktop/UpdateCheckerTest.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (c) 2026 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package org.meshtastic.desktop
+
+import kotlin.test.Test
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+class UpdateCheckerTest {
+
+ @Test
+ fun `newer patch release is an update`() {
+ assertTrue(UpdateChecker.isNewer(latest = "v2.8.1", current = "2.8.0"))
+ }
+
+ @Test
+ fun `same version is not an update`() {
+ assertFalse(UpdateChecker.isNewer(latest = "v2.8.0", current = "2.8.0"))
+ }
+
+ @Test
+ fun `older release is not an update`() {
+ assertFalse(UpdateChecker.isNewer(latest = "v2.7.14", current = "2.8.0"))
+ }
+
+ @Test
+ fun `channel suffixes are ignored when comparing`() {
+ assertFalse(UpdateChecker.isNewer(latest = "v2.8.0", current = "2.8.0-internal.24"))
+ assertTrue(UpdateChecker.isNewer(latest = "v2.8.1", current = "2.8.0-internal.24"))
+ }
+
+ @Test
+ fun `components compare numerically not lexically`() {
+ assertTrue(UpdateChecker.isNewer(latest = "v2.10.0", current = "2.9.9"))
+ assertTrue(UpdateChecker.isNewer(latest = "v3.0.0", current = "2.99.0"))
+ }
+
+ @Test
+ fun `unparseable versions never trigger an update`() {
+ assertFalse(UpdateChecker.isNewer(latest = "snapshot", current = "2.8.0"))
+ assertFalse(UpdateChecker.isNewer(latest = "v2.8.1", current = "unknown"))
+ }
+
+ @Test
+ fun `oversized version components never trigger an update`() {
+ assertFalse(UpdateChecker.isNewer(latest = "v99999999999999999999.0.0", current = "2.8.0"))
+ }
+}

diff --git a/scripts/verify-flatpak/desktop-offline.yaml b/scripts/verify-flatpak/desktop-offline.yaml
index bb6b47c906..8b2f041fb2 100644
--- a/scripts/verify-flatpak/desktop-offline.yaml
+++ b/scripts/verify-flatpak/desktop-offline.yaml
@@ -83,7 +83,9 @@ modules:
's/^\(\s*\)vendor\.set(JvmVendorSpec\.JETBRAINS)/\1\/\/ vendor.set(JvmVendorSpec.JETBRAINS)/'
desktopApp/build.gradle.kts
# Force Gradle to resolve from the bundled offline-repository ONLY (true offline test).
- - ./gradlew --offline :desktopApp:packageUberJarForCurrentOS
+ # -Pdesktop.release=true: shipped builds must have IS_DEBUG=false (no debug HTTP
+ # logging, enables the in-app update check). Mirror this in the upstream Flathub manifest.
+ - ./gradlew --offline -Pdesktop.release=true :desktopApp:packageUberJarForCurrentOS
- >
JAR_FILE=$(find desktopApp/build/compose/jars/ -name "*.jar" -type f | head -1)
&& install -Dm755 "$JAR_FILE" /app/lib/meshtastic-desktop.jar

Served by rngit 1.5.3 - Generated in 0.34s